import copy
import logging
import re

import scrapy
from utils.tools import log
from apps.tender_project.tender_project.utils.com_name import CompanyNameMixIn
from apps.tender_project.tender_project.utils.date import DateMixIn
from apps.tender_project.tender_project.utils.duration import DurationMixIn
from apps.tender_project.tender_project.utils.get_ import GetMixIN
from apps.tender_project.tender_project.utils.price import PriceMixIn
from apps.tender_project.tender_project.utils.re_ import ReMixIn
from apps.tender_project.tender_project.utils.table_ import TableMixin
from apps.tender_project.tender_project.utils.tools import smart_backtracking as temp_smart_backtracking


class SpiderALLMixIN(
    ReMixIn, GetMixIN, CompanyNameMixIn, PriceMixIn, DurationMixIn, DateMixIn, TableMixin
):
    """
    聚合所有的自己写的爬虫工具(函数)
    """
    Request = scrapy.Request  # 指定请求对象。（主要是为了写爬虫时更方便，不用再去import，而且利于日后定制/重写请求类）

    # 下面这三个只是声明一下，不使用(单纯为了兼容一些函数)
    Item = None
    logger = log
    log = log

    @classmethod
    def gen_item_with_reg_in_response(cls, response, reg_dict, item=None, need_reverse=None):
        """
        直接根据响应进行正则匹配，返回一个item
        """
        if not need_reverse:
            need_reverse = []
        item = response.item if not item else item
        return cls.gen_item_with_reg(response.soup.text, reg_dict, item, need_reverse)

    @classmethod
    def gen_item_with_reg(cls, string_content, reg_dict, item,
                          need_reverse=None,
                          need_2list=None,
                          single_line_keys=None,
                          not_replace=None,
                          need_detail=None,
                          need_filter=None,
                          need_check=None,
                          html=None):
        """
        根据传入的正则表达式 字典 对string_content进行匹配，最后返回item。
        字典的值（正则表达式）可以是字符串或字符串组成的列表，正则表达式列表优先级为从左到右，有结果立刻返回。

        :param string_content: str对象，需要匹配的文本
        :param reg_dict: dict 对象，由字段名和正则表达式列表组成的字典
        :param item: ScrapyItem 对象
        :param need_reverse: 需要倒序取的字段列表。将会对列表中的字段取匹配结果的最后一个，类型为str。适用于一个详情页只生成一个 item 的情况
        :param need_2list: 需要返回结果列表的字段列表。将会返回第一个不为空的结果列表，类型为list，此时need_reverse参数将失效。适用于 http://www.ygcgfw.com/gggs/001001/001001003/20200601/4ab57080-2407-44b6-9207-ff8e8872f0a7.html
        :param single_line_keys: 需要匹配的多行的字段列表。
        :param not_replace: 不需要替换的字段列表，如果字段有初始值则不会替换。适用于传入已经过解析的 item。
        :param need_detail: 需要返回被匹配到的pattern和结果的字段列表，返回类型为元组 (pattern, 结果)，元组中结果的类型仍会受 need_2list 影响。适用于 debug 或者分辨中标与候选。
        :param need_filter: 需要去重的字段列表，会返回一个不重复的列表。适用于 http://ggzy.jishui.gov.cn/news-show-6018.html
        :param need_check: 需要检查的字段列表，将会使用 backtrack 方法以 html 或 string_content 进行检查。
        :param html: 网页源代码，用于backtrack检查，不传入则会使用 string_content
        :return: str:    ''                       （默认）
                list:   ['']                     （取决于参数 tolist）
                tuple:  ('', '') 或 ('', [''])   （取决于参数 tolist 和 detail ）
        """
        if need_reverse == 'all':
            need_reverse = [i for i in reg_dict]
        elif not need_reverse:
            need_reverse = []

        if need_2list == 'all':
            need_2list = [i for i in reg_dict]
        elif not need_2list:
            need_2list = []

        if single_line_keys == 'all':
            single_line_keys = [i for i in reg_dict]
        elif not single_line_keys:
            single_line_keys = []

        if not_replace == 'all':
            not_replace = [i for i in reg_dict]
        elif not not_replace:
            not_replace = []

        if need_detail == 'all':
            need_detail = [i for i in reg_dict]
        elif not need_detail:
            need_detail = []

        if need_filter == 'all':
            need_filter = [i for i in reg_dict]
        elif not need_filter:
            need_filter = []

        if need_check == 'all':
            need_check = [i for i in reg_dict]
        elif not need_check:
            need_check = []

        item_ = item.copy()
        for key, pattern_list in reg_dict.items():
            reverse = False if key not in need_reverse else True
            tolist = False if key not in need_2list else True
            flag = 0 if key not in single_line_keys else re.S
            replace = True if key not in not_replace else False
            detail = False if key not in need_detail else True
            filter = False if key not in need_filter else True
            check = False if key not in need_check else True

            result = cls.reg_one(pattern_list, string_content, tolist, reverse, flag, detail, filter)
            if isinstance(result, str):
                result = result.strip()  # 去空
            if check:
                if key == 'com_name':
                    if isinstance(result, list):
                        result = [temp_smart_backtracking(html or string_content, i, reverse=True) for i in result]
                    elif isinstance(result, tuple):
                        s, l_ = result
                        result = (s, [temp_smart_backtracking(html or string_content, i, reverse=True) for i in l_])
                    else:
                        result = temp_smart_backtracking(html or string_content, result, reverse=True)
                else:
                    if isinstance(result, list):
                        result = [temp_smart_backtracking(html or string_content, i) for i in result]
                    elif isinstance(result, tuple):
                        s, l_ = result
                        result = (s, [temp_smart_backtracking(html or string_content, i) for i in l_])
                    else:
                        result = temp_smart_backtracking(html or string_content, result)
            if replace:
                item_[key] = result or item_.get(key, '')
            else:
                item_[key] = item_.get(key, '') or result

        return item_

    def gen_item_with_table(self, table,
                            rules: dict,
                            item,
                            reverse: bool = False,
                            index: int = 1,
                            min_td: int = 3,
                            more: int or bool = False,
                            fail_count: int = 1,
                            info=False,
                            pattern=''):
        """
        传入指向目标table标签的 SelectorList 对象，根据 rules 解析表格，返回 item 或者由 item.copy() 组成的列表。

        :param table: SelectorList 对象
        :param rules: dict 对象 传入其他 bool 为空的对象则使用默认字典
                    {index: [[in list], [not in list]]} (字典的值也可以由任意 Iterable 组成，可以为空但必须要有)
                    例子：
                    rules = {
                        'com_name': [['供应商', '单位名称', '中标候选人', '中标人', '中标单位'], ['地址']],
                        'price': [['总价', '元', '结果', '金额'], ['费率']],
                    }
        :param item: ScrapyItem 对象
        :param reverse: bool 对象 控制索引方向，默认为正向。部分表格含有 colspan 时，可以 flag=False 使用负索引，适用于 http://www.ygcgfw.com/gggs/001001/001001003/20200721/6855.html
        :param index: bool 对象 控制索引行，默认索引行为第一行，会被 pattern 参数的匹配结果取代
        :param more: int 或 bool 对象 控制是否继续取行以及需要额外取的行数，默认仅取索引行的下一行，设置 more=2 则返回一个最多含有三个item对象的结果列表，设置 more=True 则会一直取到最后一行
        :param min_td: int 对象 用于判断需要多少列才会被作为有效行，默认为至少需要 3 列
        :param fail_count: int 对象 控制取索引行的下一有效行的重试次数，默认重试一次
        :param info: bool 对象 控制是否输出 log 级别为 INFO 的解析结果
        :param pattern: str 对象 用于正则寻找索引行，匹配结果会取代 index 参数，适用于 http://www.ygcgfw.com/gggs/001001/001001004/20191121/d35926ac-fcbb-4f7f-84ac-241e5788a1cd.html
        :return: ScrapyItem 对象
        """
        if not rules:
            rules = {
                'com_name': [
                    ['中标人', '中标单位', '供应商', '成交单位', '成交供应商', '成交人', '单位名称', '中标候选'],
                    ['地址', '代码', '排名', '证书', '标的', '顺序', '标段', '情况']
                ],
                'price': [
                    ['总价', '元', '结果', '金额', '成交价', '标报价', '成交总额', '中标价', '价格', '成交报价',
                     '单价'], ['评', '审查']
                ],
                'duration': [
                    ['工期'], []
                ],
                'ar_name': [
                    ['项目经理', '负责人', '姓名'], ['号']
                ],
                'registration_num': [
                    ['标编号', '（包）编号'], []
                ],
            }
        if table:
            tr = table[0].xpath('.//tr')
            if pattern:
                for i in range(len(tr)):
                    index_text = tr[i].xpath('string(./td[1])').get().strip()
                    if len(index_text) < 30 and re.search(pattern, index_text):
                        index = i + 1
                        break
            if not tr:
                return [item] if more else item
            td = tr[index - 1].xpath('./th') or tr[index - 1].xpath('./td')  # 取横向表格索引
            dic = dict()  # 索引目录
            item_list = list()
            f = False  # 单位万元
            l_ = len(td)

            # 寻找索引并填充位置
            for k, v in rules.items():
                for i in range(l_):
                    ind = td[i].xpath('string(.)').get()
                    td_index = ''.join(ind.split())
                    if any(j in td_index for j in v[0]) and not any(j in td_index for j in v[1]):
                        dic[k] = i if not reverse else i - l_
                        if '万' in td_index and k in ['price', 'mana_results']:
                            f = True  # 单位万元

            if info:
                msg = f'\n解析结果: {item.get("original_url", "")}\n从第 {index} 行开始'
                for k, v in dic.items():
                    msg += f"\n{k:10}在第 {v + 1 if not reverse else v} 列"
                self.log(message=msg, level=logging.INFO)

            # 默认仅取index下一行，也可使用more=True取所有行
            more_ = more
            desc_IndexError = ""
            count = 1
            for i in tr[index:]:
                td_ = i.xpath('./td')
                len_ = len(td_)
                item_ = copy.deepcopy(item)
                if td_ and count == 1:  # 这里判断一下是否有表格不规范，比如第一行的表格占用了很多行，导致程序解析错误，这里是把第一行的数据存起来，如果程序异常，就把数据赋给异常行
                    desc_IndexError = td_[len_ - 1].xpath('string(.)').get().strip()
                count += 1
                if len_ >= min_td:  # 检查列数 防止出现无效tr或竖向表格

                    for k, v in dic.items():
                        try:
                            item_[k] = td_[v].xpath('string(.)').get().strip() or item.get(k, '')
                        except IndexError:
                            # self.log(
                            #     message=f'''表格解析出错: {k} 是否在第 {v + 1 if not reverse else v} 列？\nError url: {item.get(
                            #         "original_url", "")}''', level=logging.ERROR)
                            item_[k] = desc_IndexError

                    if f:
                        if item_.get('price'):
                            item_['price'] += '万元'
                        elif item_.get('mana_results'):
                            item_['mana_results'] += '万元罚款'
                    item_list.append(item_)
                    if more_:
                        if not isinstance(more_, bool):
                            more_ -= 1
                    else:
                        break
                else:
                    fail_count -= 1
                    if fail_count < 0:
                        break
            if item_list:
                return item_list if more else item_list[0]

        return [item] if more else item

    def search_table(self, table_list, within: list, without: list = None, tr=(1, 100), td=(1, 100),
                     info=False) -> list:
        """
        传入表格对象列表，通过参数过滤并返回需要的表格列表。

        :param table_list: 表格元素组成的可迭代对象（列表）
        :param within: 必须有的字符列表（接受正则）
        :param without: 必须没有的字符列表（接受正则）
        :param tr: 所有表格的长度限制
        :param td: 所有行的宽度限制
        :param info: 匹配结果提示
        :return: 表格对象列表
        """
        result = list()
        for table in table_list:
            text = ''.join(table.xpath('string()').get().split())
            if bool(within) == bool(self.reg_one(within, text)) and (
                    not without or (bool(without) != bool(self.reg_one(without, text)))):
                if len(table.xpath('.//tr')) in range(*tr) and all(
                        len(i.xpath('.//td') or i.xpath('.//th')) in range(*td) for i in table.xpath('.//tr')):
                    if info:
                        self.log(message=f"匹配成功： {text[:30]}")
                    result.append(table)
        return result

    @classmethod
    def gen_item_with_align2first(cls, item, key='com_name', auto_wash=False, text='', filling=''):
        '''
        会自动寻找 item 中所有值为列表的字段，生成一个对齐主键长度的生成器对象。对于除了主键之外的字段，长于主键的部分舍去，短于主键的部分用 filling 填充。
        同时内部支持调用安全的方法检查 com_name 和 ar_name 字段。
        适用于 http://www.sdsdzx.com/portal.php?mod=view&aid=10921
        经过 gen_item_with_reg(..., need_2list='all'/['com_name']) 处理。\n
        举例：
        item = {'com_name':['山东电建建设集团有限公司','兖矿东华建设有限公司','山东锦建建设有限公司'], 'price':['657.8万元','679万元']}\n
        yield from gen_item_with_align2first(item)

        >>  {'com_name': '山东电建建设集团有限公司', 'price': '657.8万元'}
            {'com_name': '兖矿东华建设有限公司', 'price': '679万元'}
            {'com_name': '中体彩印务技术有限公司 ', 'price': ''}

        :param item: ScrapyItem 对象
        :param key: 对齐的主键
        :param auto_wash: 自动清洗
        :param text: 网页文本，若传入则会影响 auto_wash 调用的清洗方法，为空时使用默认清洗方法
        :param filling: 长度少于主键时副键的填充值
        :return: generator 对象，需要直接返回的话使用 yield from
        '''
        if not isinstance(item.get(key), list):  # 其实也可以是 tuple，这里规定严格一些
            raise TypeError(f"Argument '{key}' must be a list")
        others = [i for i in item.keys() if (i != key and isinstance(item[i], list))]
        data = cls.align2first(item.get(key, []), *(item[i] for i in others), filling=filling)
        for i in data:
            _item = item.copy()
            _item[key] = i[0]
            for j in range(len(others)):
                _item[others[j]] = i[j + 1]
            if auto_wash:
                _item['com_name'] = temp_smart_backtracking(text, _item.get('com_name', ''),
                                                            reverse=True) if text else cls.wash_company(
                    _item.get('com_name', ''))
                _item['ar_name'] = temp_smart_backtracking(text, _item.get('ar_name', '')) if text else _item['ar_name']
            yield _item

    @classmethod
    def try2split_com_name(cls, item, key='com_name', sep=r'(?:[（(][联合体牵头人主成员]+[)）]|[/、&和与;；：，,\+])',
                           ignore='和与',
                           extra: list = None,
                           auto_wash=False, text=''):
        '''
        会使用 sep 参数正则分割 item 中的主键 key ，也可对其他键同时进行分割，最后调用 align2first 对齐切割后的主键长度。\n
        举例：
        item = {'com_name':'东港股份有限公司/鸿博股份有限公司/中体彩印务技术有限公司', 'price':'409500,401600'}
        yield from try2split_com_name(item, ignore='和与', extra=['price'])

        >>  {'com_name': '东港股份有限公司', 'price': '409500'}
            {'com_name': '鸿博股份有限公司', 'price': '401600'}
            {'com_name': '中体彩印务技术有限公司 ', 'price': ''}

        :param item: ScrapyItem 对象
        :param key: 分割主键，默认为 com_name
        :param sep: 自定义分割正则表达式
        :param ignore: 需要忽略的字符所组成的字符串，默认为 ignore='和与'
        :param extra: 需要额外同时分割的字段名列表
        :param auto_wash: 是否自动清洗，传递给 gen_item_with_align2first
        :param text: 网页文本，若传入则会影响 auto_wash 调用的清洗方法，为空时使用默认清洗方法
        :return: generator 对象，需要直接返回的话使用 yield from
        '''
        if ignore:
            sep = re.sub(f'[{ignore}]', '', sep)
        if not isinstance(extra, list):
            extra = []
        item[key] = [i for i in re.split(sep, item.get(key, '')) if bool(i)]
        if len(item[key]) == 1:
            _item = item.copy()
            _item[key] = item[key][0]
            if auto_wash:
                _item['com_name'] = temp_smart_backtracking(text, _item.get('com_name', ''),
                                                            reverse=True) if text else cls.wash_company(
                    _item.get('com_name', ''))
                _item['ar_name'] = temp_smart_backtracking(text, _item.get('ar_name', '')) if text else _item['ar_name']
            yield _item
        else:
            for i in extra:
                item[i] = re.split(sep, item.get(i, ''))
            yield from cls.gen_item_with_align2first(item, key, auto_wash=auto_wash, text=text)

    def get_current_text(self, ):
        ...

    @staticmethod
    def del_html_tag(content, save_line_break=True, save_p=False, save_img=False):
        """
        删除html标签
        @param content: html内容
        @param save_p: 保留p标签
        @param save_img: 保留图片标签
        @param save_line_break: 保留\n换行
        @return:
        """
        if not content:
            return content
        # js
        content = re.sub(r"(?i)<script(.|\n)*?</script>", "", content)  # (?)忽略大小写
        # css
        content = re.sub(r"(?i)<style(.|\n)*?</style>", "", content)  # (?)忽略大小写
        # 注释
        content = re.sub(r"<!--(.|\n)*?-->", "", content)
        # 干掉&nbsp;等无用的字符 但&xxx= 这种表示参数的除外
        content = re.sub(r"(?!&[a-z]+=)&[a-z]+;?", "", content)

        if save_p and save_img:
            content = re.sub(r"<(?!(p[ >]|/p>|img ))(.|\n)+?>", "", content)
        elif save_p:
            content = re.sub(r"<(?!(p[ >]|/p>))(.|\n)+?>", "", content)
        elif save_img:
            content = re.sub(r"<(?!img )(.|\n)+?>", "", content)
        elif save_line_break:
            content = re.sub(r"<(?!/?u>)(.|\n)+?>", "\n", content)
            content = re.sub(r"</p>", "\n", content)
            content = re.sub(r"<[/u]+>", "<u>", content)
        else:
            content = re.sub(r"<(.|\n)*?>", "", content)

        if save_line_break:
            # 把非换行符的空白符替换为一个空格
            content = re.sub(r"[^\S\n]+", " ", content)
            # 把多个换行符替换为一个换行符 如\n\n\n 或 \n \n \n 替换为\n
            content = re.sub(r"(\n ?)+", "\n", content)
        else:
            content = re.sub(r"\s+", " ", content)
        content = content.strip()

        return content
